package com.lafeier.beefirm.parser;

import com.lafeier.beefirm.tree.ClassDef;
import com.lafeier.beefirm.tree.FieldDef;
import com.lafeier.beefirm.tree.JavaCode;
import com.lafeier.beefirm.tree.MethodDef;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.*;
import java.util.stream.Collectors;

class DrawConfig {
    public static String noteOrientation = "left";
    public static String classColor = "#white;line:black;";
    public static String commentColor = "#white";
}

@EqualsAndHashCode(callSuper = true)
@Data
public class BeefirmDraw extends Draw {

    JavaCode javaCode;

    public BeefirmDraw(JavaCode javaCode) {
        this.javaCode = javaCode;
    }


    private void draw() {
        paint("");
        paint("@startuml");
        paint(String.format("skinparam NoteBackgroundColor %s", DrawConfig.commentColor));
        paint("right header\n" +
                "chenxusheng\n" +
                "812747475@qq.com\n" +
                "endheader\n");
        javaCode.getClassList().forEach(classDef -> new ClassDraw(javaCode, classDef).draw());
        paint("@enduml");
    }

    @Override
    public String getContent() {
        draw();
        return super.getContent();
    }
}

class ClassDraw extends Draw {
    JavaCode javaCode;
    ClassDef classDef;

    public ClassDraw(JavaCode javaCode, ClassDef classDef) {
        this.javaCode = javaCode;
        this.classDef = classDef;
    }

    private void drawImplementsRelation() {
        if (classDef.getSuperInterfaceList() != null) {
            classDef.getSuperInterfaceList().forEach(superInterface -> {
                //是否自动绘制父接口
                if (!javaCode.isDefine(superInterface)) {
                    String superInterfaceDefine = String.format("interface %s %s", superInterface, DrawConfig.classColor);
                    paint(superInterfaceDefine);
                }
                String className = classDef.getClassName();
                String relation = String.format("%s <|.. %s : impl",
                        superInterface,
                        className
                );
                paint(relation);
            });
        }
    }

    private void drawExtendRelation() {
        String className = classDef.getClassName();
        String superClassName = classDef.getSuperClassName();
        if (superClassName != null && !"".equals(superClassName)) {
            //是否需要自动绘制父类
            if (!javaCode.isDefine(superClassName)) {
                String superClassDefine = String.format("class %s %s", superClassName, DrawConfig.classColor);
                paint(superClassDefine);
            }
            String relation = String.format("%s <|-- %s : ext",
                    superClassName,
                    className
            );
            paint(relation);
        }
    }

    private static String resetFieldAndMethodCommentOrientation(String noteOrientation) {

        if ("bottom".equals(noteOrientation)) {
            return "left";

        } else if ("top".equals(noteOrientation)) {
            return "left";
        }
        return noteOrientation;
    }

    private void drawClassBody() {
        String className = classDef.getClassName();
        String kind = classDef.getKind();
        String abstractFlag = "";
        if (classDef.isAbstracted()) {
            abstractFlag = "abstract ";
        }

        String classStart = String.format("%s%s %s %s {", abstractFlag, kind, className, DrawConfig.classColor);
        paint(classStart);

        new FieldsDraw(classDef).draw();
        new MethodDraw(classDef).draw();

        String classEnd = "}";
        paint(classEnd);

        //clsss comment
        String classCommentJoined = classDef.getComments().stream().map(ClassDraw::befautifyCommentContent).collect(Collectors.joining("\n"));
        if (!"".equals(classCommentJoined)) {
            String noteOrientation = inferCommentOrientation(classCommentJoined);
            drawComment(className, classCommentJoined, noteOrientation);
        }

        // field comment
        Optional.ofNullable(classDef.getFieldList()).ifPresent(fieldDefs -> fieldDefs.forEach(fieldDef -> {
            String fieldCommentJoined = fieldDef.getComments().stream().map(ClassDraw::befautifyCommentContent).collect(Collectors.joining("\n"));
            String target = String.format("%s::%s",
                    className,
                    fieldDef.getFieldName());
            if (!"".equals(fieldCommentJoined.trim())) {
                String noteOrientation = inferCommentOrientation(fieldCommentJoined);
                noteOrientation = resetFieldAndMethodCommentOrientation(noteOrientation);
                drawComment(target, fieldCommentJoined, noteOrientation);
            }
        }));

        // method comment
        Optional.ofNullable(classDef.getMethodList()).ifPresent(methodDefs -> methodDefs.forEach(methodDef -> {
            String methodCommentsJoined = methodDef.getComments().stream().map(ClassDraw::befautifyCommentContent).collect(Collectors.joining("\n"));
            String allParamComment = methodDef.getParamList().stream().map(methodParam -> {
                String paramName = methodParam.getParamName();
                String paramComment = methodParam.getComments().stream().map(ClassDraw::befautifyCommentContent).collect(Collectors.joining("\n"));
                if (!"".equals(paramComment)) {
                    return String.format("@param %s %s",
                            paramName,
                            paramComment
                    );
                } else {
                    return "";
                }
            }).collect(Collectors.joining("\n"));
            if (!"".equals(allParamComment.trim())) {
                methodCommentsJoined += ("\n" + allParamComment);
            }

            String target = String.format("%s::\"%s %s\"",
                    className,
                    methodDef.getMethodName(),
                    methodDef.getParamListString(true));
            if (!"".equals(methodCommentsJoined.trim())) {
                String noteOrientation = inferCommentOrientation(methodCommentsJoined);
                noteOrientation = resetFieldAndMethodCommentOrientation(noteOrientation);
                drawComment(target, methodCommentsJoined, noteOrientation);
            }
        }));


    }

    private static List<String> falttenType(String type) {
        if (type == null) {
            return Collections.emptyList();
        }
        List<String> types = Arrays.stream(type.replaceAll(",", " ").replaceAll("<|\\[|>|\\]", " ").split(" "))
                .filter(t -> !"".equals(t)).collect(Collectors.toList());
        types.removeAll(Arrays.asList("List", "Seq", "Map", "Set"));
        return types;
    }

    /**
     * 组合关系
     *
     * @param fieldDef 字段
     */
    private void drawFieldRelation(FieldDef fieldDef) {
        String className = classDef.getClassName();
        String fieldType = fieldDef.getType();
        falttenType(fieldType).forEach(type -> {
            if (type != null && !"".equals(type) && javaCode.isDefine(type)) {
                String fieldRelation = String.format("\"%s::%s\" *.. %s : 成员:%s",
                        className,
                        fieldDef.getFieldName(),
                        type,
                        fieldDef.getFieldName()
                );
                paint(fieldRelation);
            }
        });

    }

    private void drawMethodRelation(MethodDef methodDef) {
        String returnType = methodDef.getReturnType();
        falttenType(returnType).forEach(type -> {
            if (type != null && !"".equals(type) && javaCode.isDefine(type)) {
                String methodRelation = String.format("\"%s::%s %s\" o.. %s : 返回值",
                        classDef.getClassName(),
                        methodDef.getMethodName(),
                        methodDef.getParamListString(true),
                        type
                );
                paint(methodRelation);
            }
        });

        methodDef.getParamList().forEach(methodParam -> {
            String paramType = methodParam.getParamType();
            falttenType(paramType).forEach(type -> {
                if (type != null && !"".equals(type) && javaCode.isDefine(type)) {
                    String methodRelation = String.format("\"%s::%s %s\" o.. %s : 参数:%s",
                            classDef.getClassName(),
                            methodDef.getMethodName(),
                            methodDef.getParamListString(true),
                            type,
                            methodParam.getParamName()
                    );
                    paint(methodRelation);
                }
            });
        });

    }

    private static boolean commentWithOrientation(String content, String noteOrientation) {
        return content.trim().toLowerCase(Locale.ROOT).startsWith("+" + noteOrientation.toLowerCase(Locale.ROOT));
    }

    private void drawComment(String target, String content, String noteOrientation) {
        String paintComment = content;
        // 去掉方向定义
        String noteOrientationDef = "+" + noteOrientation;
        if (content.contains(noteOrientationDef)) {
            paintComment = paintComment.substring(noteOrientationDef.length());
        }
        String paintContent = String.format("note %s of %s \n" +
                        "%s \n" +
                        "end note\n",
                noteOrientation,
                target,
                paintComment);
        paint(paintContent);
    }

    private static String inferCommentOrientation(String content) {
        String noteOrientation = DrawConfig.noteOrientation;
        if (commentWithOrientation(content, "left")) {
            noteOrientation = "left";
        } else if (commentWithOrientation(content, "right")) {
            noteOrientation = "right";
        } else if (commentWithOrientation(content, "top")) {
            noteOrientation = "top";
        } else if (commentWithOrientation(content, "bottom")) {
            noteOrientation = "bottom";
        }
        return noteOrientation;
    }

    /**
     * 去除首尾空字符以及注释标记
     *
     * @param content 原注释内容
     * @return 美化后的注释内容
     */
    private static String befautifyCommentContent(String content) {
        String beautifyComment = content.trim();
        if (content.startsWith("//")) {
            beautifyComment = beautifyComment.substring(2);
        } else if (content.startsWith("/*")) {
            beautifyComment = beautifyComment.substring(2);
            beautifyComment = beautifyComment.substring(0, beautifyComment.length() - 2);
        }
        beautifyComment = beautifyComment.trim();
        return beautifyComment;
    }

    void draw() {
        drawExtendRelation();
        drawImplementsRelation();
        drawClassBody();
        Optional.ofNullable(classDef.getFieldList()).orElse(new ArrayList<>()).forEach(this::drawFieldRelation);
        Optional.ofNullable(classDef.getMethodList()).orElse(new ArrayList<>()).forEach(this::drawMethodRelation);
    }

}

@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
class MethodDraw extends Draw {
    ClassDef classDef;
    List<MethodDef> methodList;

    public MethodDraw(ClassDef classDef) {
        this.classDef = classDef;
        methodList = Optional.ofNullable(classDef.getMethodList()).orElse(new ArrayList<>());
    }

    private void drawMethods() {
        methodList.forEach(this::drawMethod);
    }

    private void drawMethod(MethodDef methodDef) {

        Optional.ofNullable(methodDef.getHorizontalLine()).ifPresent(this::paint);

        String visibility = methodDef.getVisibility();
        String visibilityFlag = "+";
        if (visibility != null && !"".equals(visibility)) {
            switch (visibility) {
                case "public":
                    visibilityFlag = "+";
                    break;
                case "private":
                    visibilityFlag = "-";
                    break;
            }
        }
        String override = "";
        if (methodDef.isOverride()) {
            override = "<b><s>";
        }
        String returnType = Optional.ofNullable(methodDef.getReturnType()).orElse("");
        String methodName = methodDef.getMethodName();

        String paramList = methodDef.getParamListString(true);

        String relation = String.format("%s %s %s %s %s ",
                visibilityFlag,
                override,
                returnType,
                methodName,
                paramList);

        paint(relation);
    }

    void draw() {
        drawMethods();
    }
}

@EqualsAndHashCode(callSuper = true)
@Data
class FieldsDraw extends Draw {
    ClassDef classDef;
    List<FieldDef> fieldList;

    public FieldsDraw(ClassDef classDef) {
        this.classDef = classDef;
        fieldList = Optional.ofNullable(classDef.getFieldList()).orElse(new ArrayList<>());
    }

    private void drawFields() {
        fieldList.forEach(this::drawField);
    }

    private void drawField(FieldDef fieldDef) {

        Optional.ofNullable(fieldDef.getHorizontalLine()).ifPresent(this::paint);

        String visibility = fieldDef.getVisibility();
        String visibilityFlag = "+";
        if (visibility != null && !"".equals(visibility)) {
            switch (visibility) {
                case "public":
                    visibilityFlag = "+";
                    break;
                case "private":
                    visibilityFlag = "-";
                    break;
            }
        }
        String override = "";
        if (fieldDef.isOverride()) {
            override = "<b><s>";
        }

        String type = Optional.ofNullable(fieldDef.getType()).orElse("");
        String fieldName = fieldDef.getFieldName();
        String relation = String.format("%s %s %s %s",
                visibilityFlag,
                override,
                type,
                fieldName);
        paint(relation);
    }

    void draw() {
        drawFields();
    }
}

abstract class Draw {
    final static StringBuilder sb = new StringBuilder();

    void paint(String relation) {
        sb.append(relation)
                .append("\n");
    }

    public String getContent() {
        return sb.toString();
    }

}

